package org.naxx.security.parser;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

import org.apache.log4j.Logger;
import org.naxx.security.datatype.DataTypeResolver;
import org.naxx.security.datatype.DataTypeValueResolver;
import org.naxx.security.expression.ExpressionTree;
import org.naxx.security.expression.Node.NodeType;
import org.naxx.security.expression.impl.ExpressionTreeImpl;
import org.naxx.security.expression.impl.NodeImpl;
import org.naxx.security.function.FunctionTypeResolver;
import org.naxx.security.model.impl.match.AttributeDesignatorImpl;
import org.naxx.security.model.impl.match.ValueHolderImpl;
import org.naxx.security.parser.util.QNames;

public final class ApplyParser extends AbstractXmlParser<ExpressionTree> {
	
	private static final Logger logger = Logger.getLogger(ApplyParser.class);
	
	private DataTypeResolver dataTypeResolver;
	private DataTypeValueResolver dataTypeValueResolver;
	private FunctionTypeResolver functionTypeResolver;
	
	public void setDataTypeResolver(DataTypeResolver dataTypeResolver) {
		this.dataTypeResolver = dataTypeResolver;
	}
	public void setDataTypeValueResolver(DataTypeValueResolver dataTypeValueResolver) {
		this.dataTypeValueResolver = dataTypeValueResolver;
	}
	public void setFunctionTypeResolver(FunctionTypeResolver functionTypeResolver) {
		this.functionTypeResolver = functionTypeResolver;
	}

	/**
	 * Precondition: Cursor points to element just before first Apply element.<br>
	 * Postcondition: Cursor points to end of first Apply element.
	 */
	public ExpressionTree parse(XMLEventReader reader) throws Exception {
		ExpressionTreeImpl expressionTree = new ExpressionTreeImpl();
		XMLEvent event = reader.nextTag();
		StartElement start = event.asStartElement();
		Attribute functionIdAttr = start.getAttributeByName(QNames.FUNCTION_ID_ATTRIBUTE);
		NodeImpl root = new NodeImpl();
		root.setNodeType(NodeType.NON_LEAF);
		root.setFunctionType(this.functionTypeResolver.resolve(functionIdAttr.getValue()));
		
		expressionTree.setRoot(root);
		
		this.traverse(reader, root, expressionTree);
		
		return expressionTree;
	}
	
	/**
	 * Precondition: Cursor points to element just before Expression element.<br>
	 * Postcondition: Cursor points to closing Expression element.
	 */
	void traverse(XMLEventReader reader, NodeImpl parent, ExpressionTree expressionTree) throws Exception {
		XMLEvent event = reader.nextTag(); 
		StartElement start = event.asStartElement();
		String localPart = start.getName().getLocalPart();
		if("Apply".equals(localPart)) {
			Attribute functionIdAttr = start.getAttributeByName(QNames.FUNCTION_ID_ATTRIBUTE);
			NodeImpl node = new NodeImpl();
			node.setNodeType(NodeType.NON_LEAF);
			node.setParent(parent);
			node.setFunctionType(this.functionTypeResolver.resolve(functionIdAttr.getValue()));
			parent.addChild(node);
			
			this.skipCharactersComments(reader);
			if(reader.peek().getEventType() == XMLStreamConstants.START_ELEMENT) { // child
				this.traverse(reader, node, expressionTree);
			}
			// cursor now points just before closing Apply element
			event = reader.nextTag(); // now at closing Apply element
			this.skipCharactersComments(reader);
			if(reader.peek().getEventType() == XMLStreamConstants.START_ELEMENT) { // sibling
				this.traverse(reader, parent, expressionTree);
			}
		}
		else if("AttributeValue".equals(localPart)) {
			Attribute dataTypeAttr = start.getAttributeByName(QNames.DATA_TYPE_ATTRIBUTE);
			StringBuilder buffer = new StringBuilder();
			event = this.getCharacters(reader, buffer);
			
			NodeImpl node = new NodeImpl();
			node.setNodeType(NodeType.LEAF);
			node.setParent(parent);
			ValueHolderImpl valueHolder = new ValueHolderImpl();
			valueHolder.setDataType(this.dataTypeResolver.resolveDataType(dataTypeAttr.getValue()));
//			valueHolder.setValue(InternalServices.getDataTypeValueResolve().resolveValue(valueHolder.getDataType(), buffer.toString()));
			valueHolder.setValue(this.dataTypeValueResolver.resolveValue(valueHolder.getDataType(), buffer.toString()));
			node.setValueHolder(valueHolder);
			
			parent.addChild(node);
			
			this.skipCharactersComments(reader);
			if(reader.peek().getEventType() == XMLStreamConstants.START_ELEMENT) { // sibling
				this.traverse(reader, parent, expressionTree);
			}
			else {
				return;
			}
		}
		else if("Function".equals(localPart)) {
			Attribute functionIdAtt = start.getAttributeByName(QNames.FUNCTION_ID_ATTRIBUTE);
			NodeImpl node = new NodeImpl();
			node.setNodeType(NodeType.LEAF);
			node.setParent(parent);
			node.setFunctionType(this.functionTypeResolver.resolve(functionIdAtt.getValue()));

			parent.addChild(node);
			
			event = reader.nextTag(); // closing Function tag
			if(reader.peek().getEventType() == XMLStreamConstants.START_ELEMENT) { // sibling
				this.traverse(reader, parent, expressionTree);
			}
			else {
				return;
			}
		}
		else if("ActionAttributeDesignator".equals(localPart) ||
				"EnvironmentAttributeDesignator".equals(localPart) ||
				"ResourceAttributeDesignator".equals(localPart) ||
				"SubjectAttributeDesignator".equals(localPart)) {
			NodeImpl node = new NodeImpl();
			node.setAttributeDesignator(this.resolve(start));
			node.setNodeType(NodeType.LEAF);
			node.setParent(parent);
			parent.addChild(node);
			event = reader.nextTag(); // now at closing 
			this.skipCharactersComments(reader);
			if(reader.peek().getEventType() == XMLStreamConstants.START_ELEMENT) { // sibling
				this.traverse(reader, parent, expressionTree);
			}
			else {
				return;
			}
		}
		else {
			logger.warn("SKIPPING SOME PART");
			loop: 
			while(true) {
				event = reader.nextEvent();
				if(event.isEndElement()) {
					if(localPart.equals(event.asEndElement().getName().getLocalPart())) {
						break loop;
					}
				}
			}
		}
	}
	
	private AttributeDesignatorImpl resolve(StartElement start) {
		AttributeDesignatorImpl designator = new AttributeDesignatorImpl();
		designator.setAttributeId(start.getAttributeByName(QNames.ATTRIBUTE_ID_ATTRIBUTE).getValue());
		designator.setDataType(this.dataTypeResolver.resolveDataType(start
				.getAttributeByName(QNames.DATA_TYPE_ATTRIBUTE).getValue()));
		
		Attribute issuerAttr = start.getAttributeByName(QNames.ISSUER_ATTRIBUTE);
		designator.setIssuer((issuerAttr != null ? issuerAttr.getValue() : null));
		
		Attribute mustBePresentAttr = start.getAttributeByName(QNames.MUST_BE_PRESENT_ATTRIBUTE);
		designator.setMustBePresent((mustBePresentAttr != null ? Boolean.valueOf(mustBePresentAttr.getValue()) : false));
		
		return designator;
	}
}
